import arraySlice from '../Core/arraySlice.js';
import BoundingSphere from '../Core/BoundingSphere.js';
import Cartesian3 from '../Core/Cartesian3.js';
import Cartesian4 from '../Core/Cartesian4.js';
import Color from '../Core/Color.js';
import combine from '../Core/combine.js';
import ComponentDatatype from '../Core/ComponentDatatype.js';
import defaultValue from '../Core/defaultValue.js';
import defined from '../Core/defined.js';
import defineProperties from '../Core/defineProperties.js';
import destroyObject from '../Core/destroyObject.js';
import DeveloperError from '../Core/DeveloperError.js';
import FeatureDetection from '../Core/FeatureDetection.js';
import IndexDatatype from '../Core/IndexDatatype.js';
import Matrix4 from '../Core/Matrix4.js';
import PrimitiveType from '../Core/PrimitiveType.js';
import RuntimeError from '../Core/RuntimeError.js';
import Transforms from '../Core/Transforms.js';
import WebGLConstants from '../Core/WebGLConstants.js';
import addDefaults from '../ThirdParty/GltfPipeline/addDefaults.js';
import ForEach from '../ThirdParty/GltfPipeline/ForEach.js';
import getAccessorByteStride from '../ThirdParty/GltfPipeline/getAccessorByteStride.js';
import numberOfComponentsForType from '../ThirdParty/GltfPipeline/numberOfComponentsForType.js';
import parseGlb from '../ThirdParty/GltfPipeline/parseGlb.js';
import updateVersion from '../ThirdParty/GltfPipeline/updateVersion.js';
import when from '../ThirdParty/when.js';
import Axis from './Axis.js';
import ModelLoadResources from './ModelLoadResources.js';
import ModelUtility from './ModelUtility.js';
import processModelMaterialsCommon from './processModelMaterialsCommon.js';
import processPbrMaterials from './processPbrMaterials.js';
import SceneMode from './SceneMode.js';
import Vector3DTileBatch from './Vector3DTileBatch.js';
import Vector3DTilePrimitive from './Vector3DTilePrimitive.js';

    var boundingSphereCartesian3Scratch = new Cartesian3();

    var ModelState = ModelUtility.ModelState;

    ///////////////////////////////////////////////////////////////////////////

    /**
     * A 3D model for classifying other 3D assets based on glTF, the runtime 3D asset format.
     * This is a special case when a model of a 3D tileset becomes a classifier when setting {@link Cesium3DTileset#classificationType}.
     *
     * @alias ClassificationModel
     * @constructor
     *
     * @private
     *
     * @param {Object} options Object with the following properties:
     * @param {ArrayBuffer|Uint8Array} options.gltf A binary glTF buffer.
     * @param {Boolean} [options.show=true] Determines if the model primitive will be shown.
     * @param {Matrix4} [options.modelMatrix=Matrix4.IDENTITY] The 4x4 transformation matrix that transforms the model from model to world coordinates.
     * @param {Boolean} [options.debugShowBoundingVolume=false] For debugging only. Draws the bounding sphere for each draw command in the model.
     * @param {Boolean} [options.debugWireframe=false] For debugging only. Draws the model in wireframe.
     * @param {ClassificationType} [options.classificationType] What this model will classify.
     *
     * @exception {RuntimeError} Only binary glTF is supported.
     * @exception {RuntimeError} Buffer data must be embedded in the binary glTF.
     * @exception {RuntimeError} Only one node is supported for classification and it must have a mesh.
     * @exception {RuntimeError} Only one mesh is supported when using b3dm for classification.
     * @exception {RuntimeError} Only one primitive per mesh is supported when using b3dm for classification.
     * @exception {RuntimeError} The mesh must have a position attribute.
     * @exception {RuntimeError} The mesh must have a batch id attribute.
     */
    function ClassificationModel(options) {
        options = defaultValue(options, defaultValue.EMPTY_OBJECT);

        var gltf = options.gltf;
        if (gltf instanceof ArrayBuffer) {
            gltf = new Uint8Array(gltf);
        }

        if (gltf instanceof Uint8Array) {
            // Parse and update binary glTF
            gltf = parseGlb(gltf);
            updateVersion(gltf);
            addDefaults(gltf);
            processModelMaterialsCommon(gltf);
            processPbrMaterials(gltf);
        } else {
            throw new RuntimeError('Only binary glTF is supported as a classifier.');
        }

        ForEach.buffer(gltf, function(buffer) {
            if (!defined(buffer.extras._pipeline.source)) {
                throw new RuntimeError('Buffer data must be embedded in the binary gltf.');
            }
        });

        var gltfNodes = gltf.nodes;
        var gltfMeshes = gltf.meshes;

        var gltfNode = gltfNodes[0];
        var meshId = gltfNode.mesh;
        if (gltfNodes.length !== 1 || !defined(meshId)) {
            throw new RuntimeError('Only one node is supported for classification and it must have a mesh.');
        }

        if (gltfMeshes.length !== 1) {
            throw new RuntimeError('Only one mesh is supported when using b3dm for classification.');
        }

        var gltfPrimitives = gltfMeshes[0].primitives;
        if (gltfPrimitives.length !== 1) {
            throw new RuntimeError('Only one primitive per mesh is supported when using b3dm for classification.');
        }

        var gltfPositionAttribute = gltfPrimitives[0].attributes.POSITION;
        if (!defined(gltfPositionAttribute)) {
            throw new RuntimeError('The mesh must have a position attribute.');
        }

        var gltfBatchIdAttribute = gltfPrimitives[0].attributes._BATCHID;
        if (!defined(gltfBatchIdAttribute)) {
            throw new RuntimeError('The mesh must have a batch id attribute.');
        }

        this._gltf = gltf;

        /**
         * Determines if the model primitive will be shown.
         *
         * @type {Boolean}
         *
         * @default true
         */
        this.show = defaultValue(options.show, true);

        /**
         * The 4x4 transformation matrix that transforms the model from model to world coordinates.
         * When this is the identity matrix, the model is drawn in world coordinates, i.e., Earth's WGS84 coordinates.
         * Local reference frames can be used by providing a different transformation matrix, like that returned
         * by {@link Transforms.eastNorthUpToFixedFrame}.
         *
         * @type {Matrix4}
         *
         * @default {@link Matrix4.IDENTITY}
         *
         * @example
         * var origin = Cesium.Cartesian3.fromDegrees(-95.0, 40.0, 200000.0);
         * m.modelMatrix = Cesium.Transforms.eastNorthUpToFixedFrame(origin);
         */
        this.modelMatrix = Matrix4.clone(defaultValue(options.modelMatrix, Matrix4.IDENTITY));
        this._modelMatrix = Matrix4.clone(this.modelMatrix);

        this._ready = false;
        this._readyPromise = when.defer();

        /**
         * This property is for debugging only; it is not for production use nor is it optimized.
         * <p>
         * Draws the bounding sphere for each draw command in the model.  A glTF primitive corresponds
         * to one draw command.  A glTF mesh has an array of primitives, often of length one.
         * </p>
         *
         * @type {Boolean}
         *
         * @default false
         */
        this.debugShowBoundingVolume = defaultValue(options.debugShowBoundingVolume, false);
        this._debugShowBoundingVolume = false;

        /**
         * This property is for debugging only; it is not for production use nor is it optimized.
         * <p>
         * Draws the model in wireframe.
         * </p>
         *
         * @type {Boolean}
         *
         * @default false
         */
        this.debugWireframe = defaultValue(options.debugWireframe, false);
        this._debugWireframe = false;

        this._classificationType = options.classificationType;

        // Undocumented options
        this._vertexShaderLoaded = options.vertexShaderLoaded;
        this._classificationShaderLoaded = options.classificationShaderLoaded;
        this._uniformMapLoaded = options.uniformMapLoaded;
        this._pickIdLoaded = options.pickIdLoaded;
        this._ignoreCommands = defaultValue(options.ignoreCommands, false);
        this._upAxis = defaultValue(options.upAxis, Axis.Y);
        this._batchTable = options.batchTable;

        this._computedModelMatrix = new Matrix4(); // Derived from modelMatrix and axis
        this._initialRadius = undefined;           // Radius without model's scale property, model-matrix scale, animations, or skins
        this._boundingSphere = undefined;
        this._scaledBoundingSphere = new BoundingSphere();
        this._state = ModelState.NEEDS_LOAD;
        this._loadResources = undefined;

        this._mode = undefined;
        this._dirty = false;                       // true when the model was transformed this frame

        this._nodeMatrix = new Matrix4();
        this._primitive = undefined;

        this._extensionsUsed = undefined;     // Cached used glTF extensions
        this._extensionsRequired = undefined; // Cached required glTF extensions
        this._quantizedUniforms = undefined;  // Quantized uniforms for WEB3D_quantized_attributes

        this._buffers = {};
        this._vertexArray = undefined;
        this._shaderProgram = undefined;
        this._uniformMap = undefined;

        this._geometryByteLength = 0;
        this._trianglesLength = 0;

        // CESIUM_RTC extension
        this._rtcCenter = undefined;    // reference to either 3D or 2D
        this._rtcCenterEye = undefined; // in eye coordinates
        this._rtcCenter3D = undefined;  // in world coordinates
        this._rtcCenter2D = undefined;  // in projected world coordinates
    }

    defineProperties(ClassificationModel.prototype, {
        /**
         * The object for the glTF JSON, including properties with default values omitted
         * from the JSON provided to this model.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Object}
         * @readonly
         *
         * @default undefined
         */
        gltf : {
            get : function() {
                return this._gltf;
            }
        },

        /**
         * The model's bounding sphere in its local coordinate system.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {BoundingSphere}
         * @readonly
         *
         * @default undefined
         *
         * @exception {DeveloperError} The model is not loaded.  Use ClassificationModel.readyPromise or wait for ClassificationModel.ready to be true.
         *
         * @example
         * // Center in WGS84 coordinates
         * var center = Cesium.Matrix4.multiplyByPoint(model.modelMatrix, model.boundingSphere.center, new Cesium.Cartesian3());
         */
        boundingSphere : {
            get : function() {
                //>>includeStart('debug', pragmas.debug);
                if (this._state !== ModelState.LOADED) {
                    throw new DeveloperError('The model is not loaded.  Use ClassificationModel.readyPromise or wait for ClassificationModel.ready to be true.');
                }
                //>>includeEnd('debug');

                var modelMatrix = this.modelMatrix;
                var nonUniformScale = Matrix4.getScale(modelMatrix, boundingSphereCartesian3Scratch);

                var scaledBoundingSphere = this._scaledBoundingSphere;
                scaledBoundingSphere.center = Cartesian3.multiplyComponents(this._boundingSphere.center, nonUniformScale, scaledBoundingSphere.center);
                scaledBoundingSphere.radius = Cartesian3.maximumComponent(nonUniformScale) * this._initialRadius;

                if (defined(this._rtcCenter)) {
                    Cartesian3.add(this._rtcCenter, scaledBoundingSphere.center, scaledBoundingSphere.center);
                }

                return scaledBoundingSphere;
            }
        },

        /**
         * When <code>true</code>, this model is ready to render, i.e., the external binary, image,
         * and shader files were downloaded and the WebGL resources were created.  This is set to
         * <code>true</code> right before {@link ClassificationModel#readyPromise} is resolved.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Boolean}
         * @readonly
         *
         * @default false
         */
        ready : {
            get : function() {
                return this._ready;
            }
        },

        /**
         * Gets the promise that will be resolved when this model is ready to render, i.e., when the external binary, image,
         * and shader files were downloaded and the WebGL resources were created.
         * <p>
         * This promise is resolved at the end of the frame before the first frame the model is rendered in.
         * </p>
         *
         * @memberof ClassificationModel.prototype
         * @type {Promise.<ClassificationModel>}
         * @readonly
         *
         * @see ClassificationModel#ready
         */
        readyPromise : {
            get : function() {
                return this._readyPromise.promise;
            }
        },

        /**
         * Returns true if the model was transformed this frame
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Boolean}
         * @readonly
         *
         * @private
         */
        dirty : {
            get : function() {
                return this._dirty;
            }
        },

        /**
         * Returns an object with all of the glTF extensions used.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Object}
         * @readonly
         */
        extensionsUsed : {
            get : function() {
                if (!defined(this._extensionsUsed)) {
                    this._extensionsUsed = ModelUtility.getUsedExtensions(this.gltf);
                }
                return this._extensionsUsed;
            }
        },

        /**
         * Returns an object with all of the glTF extensions required.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Object}
         * @readonly
         */
        extensionsRequired : {
            get : function() {
                if (!defined(this._extensionsRequired)) {
                    this._extensionsRequired = ModelUtility.getRequiredExtensions(this.gltf);
                }
                return this._extensionsRequired;
            }
        },

        /**
         * Gets the model's up-axis.
         * By default models are y-up according to the glTF spec, however geo-referenced models will typically be z-up.
         *
         * @memberof ClassificationModel.prototype
         *
         * @type {Number}
         * @default Axis.Y
         * @readonly
         *
         * @private
         */
        upAxis : {
            get : function() {
                return this._upAxis;
            }
        },

        /**
         * Gets the model's triangle count.
         *
         * @private
         */
        trianglesLength : {
            get : function() {
                return this._trianglesLength;
            }
        },

        /**
         * Gets the model's geometry memory in bytes. This includes all vertex and index buffers.
         *
         * @private
         */
        geometryByteLength : {
            get : function() {
                return this._geometryByteLength;
            }
        },

        /**
         * Gets the model's texture memory in bytes.
         *
         * @private
         */
        texturesByteLength : {
            get : function() {
                return 0;
            }
        },

        /**
         * Gets the model's classification type.
         * @memberof ClassificationModel.prototype
         * @type {ClassificationType}
         */
        classificationType : {
            get : function() {
                return this._classificationType;
            }
        }
    });

    ///////////////////////////////////////////////////////////////////////////

    function addBuffersToLoadResources(model) {
        var gltf = model.gltf;
        var loadResources = model._loadResources;
        ForEach.buffer(gltf, function(buffer, id) {
            loadResources.buffers[id] = buffer.extras._pipeline.source;
        });
    }

    function parseBufferViews(model) {
        var bufferViews = model.gltf.bufferViews;

        var vertexBuffersToCreate = model._loadResources.vertexBuffersToCreate;

        // Only ARRAY_BUFFER here.  ELEMENT_ARRAY_BUFFER created below.
        ForEach.bufferView(model.gltf, function(bufferView, id) {
            if (bufferView.target === WebGLConstants.ARRAY_BUFFER) {
                vertexBuffersToCreate.enqueue(id);
            }
        });

        var indexBuffersToCreate = model._loadResources.indexBuffersToCreate;
        var indexBufferIds = {};

        // The Cesium Renderer requires knowing the datatype for an index buffer
        // at creation type, which is not part of the glTF bufferview so loop
        // through glTF accessors to create the bufferview's index buffer.
        ForEach.accessor(model.gltf, function(accessor) {
            var bufferViewId = accessor.bufferView;
            var bufferView = bufferViews[bufferViewId];

            if ((bufferView.target === WebGLConstants.ELEMENT_ARRAY_BUFFER) && !defined(indexBufferIds[bufferViewId])) {
                indexBufferIds[bufferViewId] = true;
                indexBuffersToCreate.enqueue({
                    id : bufferViewId,
                    componentType : accessor.componentType
                });
            }
        });
    }

    function createVertexBuffer(bufferViewId, model) {
        var loadResources = model._loadResources;
        var bufferViews = model.gltf.bufferViews;
        var bufferView = bufferViews[bufferViewId];
        var vertexBuffer = loadResources.getBuffer(bufferView);
        model._buffers[bufferViewId] = vertexBuffer;
        model._geometryByteLength += vertexBuffer.byteLength;
    }

    function createIndexBuffer(bufferViewId, componentType, model) {
        var loadResources = model._loadResources;
        var bufferViews = model.gltf.bufferViews;
        var bufferView = bufferViews[bufferViewId];
        var indexBuffer = {
            typedArray : loadResources.getBuffer(bufferView),
            indexDatatype : componentType
        };
        model._buffers[bufferViewId] = indexBuffer;
        model._geometryByteLength += indexBuffer.typedArray.byteLength;
    }

    function createBuffers(model) {
        var loadResources = model._loadResources;

        if (loadResources.pendingBufferLoads !== 0) {
            return;
        }

        var vertexBuffersToCreate = loadResources.vertexBuffersToCreate;
        var indexBuffersToCreate = loadResources.indexBuffersToCreate;

        while (vertexBuffersToCreate.length > 0) {
            createVertexBuffer(vertexBuffersToCreate.dequeue(), model);
        }

        while (indexBuffersToCreate.length > 0) {
            var i = indexBuffersToCreate.dequeue();
            createIndexBuffer(i.id, i.componentType, model);
        }
    }

    function modifyShaderForQuantizedAttributes(shader, model) {
        var primitive = model.gltf.meshes[0].primitives[0];
        var result = ModelUtility.modifyShaderForQuantizedAttributes(model.gltf, primitive, shader);
        model._quantizedUniforms = result.uniforms;
        return result.shader;
    }

    function modifyShader(shader, callback) {
        if (defined(callback)) {
            shader = callback(shader);
        }
        return shader;
    }

    function createProgram(model) {
        var gltf = model.gltf;

        var positionName = ModelUtility.getAttributeOrUniformBySemantic(gltf, 'POSITION');
        var batchIdName = ModelUtility.getAttributeOrUniformBySemantic(gltf, '_BATCHID');

        var attributeLocations = {};
        attributeLocations[positionName] = 0;
        attributeLocations[batchIdName] = 1;

        var modelViewProjectionName = ModelUtility.getAttributeOrUniformBySemantic(gltf, 'MODELVIEWPROJECTION');

        var uniformDecl;
        var toClip;

        if (!defined(modelViewProjectionName)) {
            var projectionName = ModelUtility.getAttributeOrUniformBySemantic(gltf, 'PROJECTION');
            var modelViewName = ModelUtility.getAttributeOrUniformBySemantic(gltf, 'MODELVIEW');
            if (!defined(modelViewName)) {
                modelViewName = ModelUtility.getAttributeOrUniformBySemantic(gltf, 'CESIUM_RTC_MODELVIEW');
            }

            uniformDecl =
                'uniform mat4 ' + modelViewName + ';\n' +
                'uniform mat4 ' + projectionName + ';\n';
            toClip = projectionName + ' * ' + modelViewName + ' * vec4(' + positionName + ', 1.0)';
        } else {
            uniformDecl = 'uniform mat4 ' + modelViewProjectionName + ';\n';
            toClip = modelViewProjectionName + ' * vec4(' + positionName + ', 1.0)';
        }

        var computePosition = '    vec4 positionInClipCoords = ' + toClip + ';\n';

        var vs =
            'attribute vec3 ' + positionName + ';\n' +
            'attribute float ' + batchIdName + ';\n' +
            uniformDecl +
            'void main() {\n' +
            computePosition +
            '    gl_Position = czm_depthClampFarPlane(positionInClipCoords);\n' +
            '}\n';
        var fs =
            '#ifdef GL_EXT_frag_depth\n' +
            '#extension GL_EXT_frag_depth : enable\n' +
            '#endif\n' +
            'void main() \n' +
            '{ \n' +
            '    gl_FragColor = vec4(1.0); \n' +
            '    czm_writeDepthClampedToFarPlane();\n' +
            '}\n';

        if (model.extensionsUsed.WEB3D_quantized_attributes) {
            vs = modifyShaderForQuantizedAttributes(vs, model);
        }

        var drawVS = modifyShader(vs, model._vertexShaderLoaded);
        var drawFS = modifyShader(fs, model._classificationShaderLoaded);

        drawVS = ModelUtility.modifyVertexShaderForLogDepth(drawVS, toClip);
        drawFS = ModelUtility.modifyFragmentShaderForLogDepth(drawFS);

        model._shaderProgram = {
            vertexShaderSource : drawVS,
            fragmentShaderSource : drawFS,
            attributeLocations : attributeLocations
        };
    }

    function getAttributeLocations() {
        return {
            POSITION : 0,
            _BATCHID : 1
        };
    }

    function createVertexArray(model) {
        var loadResources = model._loadResources;
        if (!loadResources.finishedBuffersCreation() || defined(model._vertexArray)) {
            return;
        }

        var rendererBuffers = model._buffers;
        var gltf = model.gltf;
        var accessors = gltf.accessors;
        var meshes = gltf.meshes;
        var primitives = meshes[0].primitives;

        var primitive = primitives[0];
        var attributeLocations = getAttributeLocations();
        var attributes = {};
        ForEach.meshPrimitiveAttribute(primitive, function(accessorId, attributeName) {
            // Skip if the attribute is not used by the material, e.g., because the asset
            // was exported with an attribute that wasn't used and the asset wasn't optimized.
            var attributeLocation = attributeLocations[attributeName];
            if (defined(attributeLocation)) {
                var a = accessors[accessorId];
                attributes[attributeName] = {
                    index: attributeLocation,
                    vertexBuffer: rendererBuffers[a.bufferView],
                    componentsPerAttribute: numberOfComponentsForType(a.type),
                    componentDatatype: a.componentType,
                    offsetInBytes: a.byteOffset,
                    strideInBytes: getAccessorByteStride(gltf, a)
                };
            }
        });

        var indexBuffer;
        if (defined(primitive.indices)) {
            var accessor = accessors[primitive.indices];
            indexBuffer = rendererBuffers[accessor.bufferView];
        }
        model._vertexArray = {
            attributes : attributes,
            indexBuffer : indexBuffer
        };
    }

    var gltfSemanticUniforms = {
        PROJECTION : function(uniformState, model) {
            return ModelUtility.getGltfSemanticUniforms().PROJECTION(uniformState, model);
        },
        MODELVIEW : function(uniformState, model) {
            return ModelUtility.getGltfSemanticUniforms().MODELVIEW(uniformState, model);
        },
        CESIUM_RTC_MODELVIEW : function(uniformState, model) {
            return ModelUtility.getGltfSemanticUniforms().CESIUM_RTC_MODELVIEW(uniformState, model);
        },
        MODELVIEWPROJECTION : function(uniformState, model) {
            return ModelUtility.getGltfSemanticUniforms().MODELVIEWPROJECTION(uniformState, model);
        }
    };

    function createUniformMap(model, context) {
        if (defined(model._uniformMap)) {
            return;
        }

        var uniformMap = {};
        ForEach.technique(model.gltf, function(technique) {
            ForEach.techniqueUniform(technique, function(uniform, uniformName) {
                if (!defined(uniform.semantic) || !defined(gltfSemanticUniforms[uniform.semantic])) {
                    return;
                }

                uniformMap[uniformName] = gltfSemanticUniforms[uniform.semantic](context.uniformState, model);
            });
        });

        model._uniformMap = uniformMap;
    }

    function createUniformsForQuantizedAttributes(model, primitive) {
        return ModelUtility.createUniformsForQuantizedAttributes(model.gltf, primitive, model._quantizedUniforms);
    }

    function triangleCountFromPrimitiveIndices(primitive, indicesCount) {
        switch (primitive.mode) {
            case PrimitiveType.TRIANGLES:
                return (indicesCount / 3);
            case PrimitiveType.TRIANGLE_STRIP:
            case PrimitiveType.TRIANGLE_FAN:
                return Math.max(indicesCount - 2, 0);
            default:
                return 0;
        }
    }

    function createPrimitive(model) {
        var batchTable = model._batchTable;

        var uniformMap = model._uniformMap;
        var vertexArray = model._vertexArray;

        var gltf = model.gltf;
        var accessors = gltf.accessors;
        var gltfMeshes = gltf.meshes;
        var primitive = gltfMeshes[0].primitives[0];
        var ix = accessors[primitive.indices];

        var positionAccessor = primitive.attributes.POSITION;
        var minMax = ModelUtility.getAccessorMinMax(gltf, positionAccessor);
        var boundingSphere = BoundingSphere.fromCornerPoints(Cartesian3.fromArray(minMax.min), Cartesian3.fromArray(minMax.max));

        var offset;
        var count;
        if (defined(ix)) {
            count = ix.count;
            offset = (ix.byteOffset / IndexDatatype.getSizeInBytes(ix.componentType));  // glTF has offset in bytes.  Cesium has offsets in indices
        }
        else {
            var positions = accessors[primitive.attributes.POSITION];
            count = positions.count;
            offset = 0;
        }

        // Update model triangle count using number of indices
        model._trianglesLength += triangleCountFromPrimitiveIndices(primitive, count);

        // Allow callback to modify the uniformMap
        if (defined(model._uniformMapLoaded)) {
            uniformMap = model._uniformMapLoaded(uniformMap);
        }

        // Add uniforms for decoding quantized attributes if used
        if (model.extensionsUsed.WEB3D_quantized_attributes) {
            var quantizedUniformMap = createUniformsForQuantizedAttributes(model, primitive);
            uniformMap = combine(uniformMap, quantizedUniformMap);
        }

        var attribute = vertexArray.attributes.POSITION;
        var componentDatatype = attribute.componentDatatype;
        var typedArray = attribute.vertexBuffer;
        var byteOffset = typedArray.byteOffset;
        var bufferLength = typedArray.byteLength / ComponentDatatype.getSizeInBytes(componentDatatype);
        var positionsBuffer = ComponentDatatype.createArrayBufferView(componentDatatype, typedArray.buffer, byteOffset, bufferLength);

        attribute = vertexArray.attributes._BATCHID;
        componentDatatype = attribute.componentDatatype;
        typedArray = attribute.vertexBuffer;
        byteOffset = typedArray.byteOffset;
        bufferLength = typedArray.byteLength / ComponentDatatype.getSizeInBytes(componentDatatype);
        var vertexBatchIds = ComponentDatatype.createArrayBufferView(componentDatatype, typedArray.buffer, byteOffset, bufferLength);

        var buffer = vertexArray.indexBuffer.typedArray;
        var indices;
        if (vertexArray.indexBuffer.indexDatatype === IndexDatatype.UNSIGNED_SHORT) {
            indices = new Uint16Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Uint16Array.BYTES_PER_ELEMENT);
        } else {
            indices = new Uint32Array(buffer.buffer, buffer.byteOffset, buffer.byteLength / Uint32Array.BYTES_PER_ELEMENT);
        }

        positionsBuffer = arraySlice(positionsBuffer);
        vertexBatchIds = arraySlice(vertexBatchIds);
        indices = arraySlice(indices, offset, offset + count);

        var batchIds = [];
        var indexCounts = [];
        var indexOffsets = [];
        var batchedIndices = [];

        var currentId = vertexBatchIds[indices[0]];
        batchIds.push(currentId);
        indexOffsets.push(0);

        var batchId;
        var indexOffset;
        var indexCount;
        var indicesLength = indices.length;
        for (var j = 1; j < indicesLength; ++j) {
            batchId = vertexBatchIds[indices[j]];
            if (batchId !== currentId) {
                indexOffset = indexOffsets[indexOffsets.length - 1];
                indexCount = j - indexOffset;

                batchIds.push(batchId);
                indexCounts.push(indexCount);
                indexOffsets.push(j);

                batchedIndices.push(new Vector3DTileBatch({
                    offset : indexOffset,
                    count : indexCount,
                    batchIds : [currentId],
                    color : Color.WHITE
                }));

                currentId = batchId;
            }
        }

        indexOffset = indexOffsets[indexOffsets.length - 1];
        indexCount = indicesLength - indexOffset;

        indexCounts.push(indexCount);
        batchedIndices.push(new Vector3DTileBatch({
            offset : indexOffset,
            count : indexCount,
            batchIds : [currentId],
            color : Color.WHITE
        }));

        var shader = model._shaderProgram;
        var vertexShaderSource = shader.vertexShaderSource;
        var fragmentShaderSource = shader.fragmentShaderSource;
        var attributeLocations = shader.attributeLocations;
        var pickId = defined(model._pickIdLoaded) ? model._pickIdLoaded() : undefined;

        model._primitive = new Vector3DTilePrimitive({
            classificationType : model._classificationType,
            positions : positionsBuffer,
            indices : indices,
            indexOffsets : indexOffsets,
            indexCounts : indexCounts,
            batchIds : batchIds,
            vertexBatchIds : vertexBatchIds,
            batchedIndices : batchedIndices,
            batchTable : batchTable,
            boundingVolume : new BoundingSphere(), // updated in update()
            _vertexShaderSource : vertexShaderSource,
            _fragmentShaderSource : fragmentShaderSource,
            _attributeLocations : attributeLocations,
            _uniformMap : uniformMap,
            _pickId : pickId,
            _modelMatrix : new Matrix4(), // updated in update()
            _boundingSphere : boundingSphere // used to update boundingVolume
        });

        // Release CPU resources
        model._buffers = undefined;
        model._vertexArray = undefined;
        model._shaderProgram = undefined;
        model._uniformMap = undefined;
    }

    function createRuntimeNodes(model) {
        var loadResources = model._loadResources;
        if (!loadResources.finished()) {
            return;
        }

        if (defined(model._primitive)) {
            return;
        }

        var gltf = model.gltf;
        var nodes = gltf.nodes;
        var gltfNode = nodes[0];
        model._nodeMatrix = ModelUtility.getTransform(gltfNode, model._nodeMatrix);

        createPrimitive(model);
    }

    function createResources(model, frameState) {
        var context = frameState.context;

        ModelUtility.checkSupportedGlExtensions(model.gltf.glExtensionsUsed, context);
        createBuffers(model); // using glTF bufferViews
        createProgram(model);
        createVertexArray(model); // using glTF meshes
        createUniformMap(model, context);  // using glTF materials/techniques
        createRuntimeNodes(model); // using glTF scene
    }

    ///////////////////////////////////////////////////////////////////////////

    var scratchComputedTranslation = new Cartesian4();
    var scratchComputedMatrixIn2D = new Matrix4();

    function updateNodeModelMatrix(model, modelTransformChanged, justLoaded, projection) {
        var computedModelMatrix = model._computedModelMatrix;

        if ((model._mode !== SceneMode.SCENE3D) && !model._ignoreCommands) {
            var translation = Matrix4.getColumn(computedModelMatrix, 3, scratchComputedTranslation);
            if (!Cartesian4.equals(translation, Cartesian4.UNIT_W)) {
                computedModelMatrix = Transforms.basisTo2D(projection, computedModelMatrix, scratchComputedMatrixIn2D);
                model._rtcCenter = model._rtcCenter3D;
            } else {
                var center = model.boundingSphere.center;
                var to2D = Transforms.wgs84To2DModelMatrix(projection, center, scratchComputedMatrixIn2D);
                computedModelMatrix = Matrix4.multiply(to2D, computedModelMatrix, scratchComputedMatrixIn2D);

                if (defined(model._rtcCenter)) {
                    Matrix4.setTranslation(computedModelMatrix, Cartesian4.UNIT_W, computedModelMatrix);
                    model._rtcCenter = model._rtcCenter2D;
                }
            }
        }

        var primitive = model._primitive;

        if (modelTransformChanged || justLoaded) {
            Matrix4.multiplyTransformation(computedModelMatrix, model._nodeMatrix, primitive._modelMatrix);
            BoundingSphere.transform(primitive._boundingSphere, primitive._modelMatrix, primitive._boundingVolume);

            if (defined(model._rtcCenter)) {
                Cartesian3.add(model._rtcCenter, primitive._boundingVolume.center, primitive._boundingVolume.center);
            }
        }
    }

    ///////////////////////////////////////////////////////////////////////////

    ClassificationModel.prototype.updateCommands = function(batchId, color) {
        this._primitive.updateCommands(batchId, color);
    };

    ClassificationModel.prototype.update = function(frameState) {
        if (frameState.mode === SceneMode.MORPHING) {
            return;
        }

        if (!FeatureDetection.supportsWebP.initialized) {
            FeatureDetection.supportsWebP.initialize();
            return;
        }
        var supportsWebP = FeatureDetection.supportsWebP();

        if ((this._state === ModelState.NEEDS_LOAD) && defined(this.gltf)) {
            this._state = ModelState.LOADING;
            if (this._state !== ModelState.FAILED) {
                var extensions = this.gltf.extensions;
                if (defined(extensions) && defined(extensions.CESIUM_RTC)) {
                    var center = Cartesian3.fromArray(extensions.CESIUM_RTC.center);
                    if (!Cartesian3.equals(center, Cartesian3.ZERO)) {
                        this._rtcCenter3D = center;

                        var projection = frameState.mapProjection;
                        var ellipsoid = projection.ellipsoid;
                        var cartographic = ellipsoid.cartesianToCartographic(this._rtcCenter3D);
                        var projectedCart = projection.project(cartographic);
                        Cartesian3.fromElements(projectedCart.z, projectedCart.x, projectedCart.y, projectedCart);
                        this._rtcCenter2D = projectedCart;

                        this._rtcCenterEye = new Cartesian3();
                        this._rtcCenter = this._rtcCenter3D;
                    }
                }

                this._loadResources = new ModelLoadResources();
                ModelUtility.parseBuffers(this);
            }
        }

        var loadResources = this._loadResources;
        var justLoaded = false;

        if (this._state === ModelState.LOADING) {
            // Transition from LOADING -> LOADED once resources are downloaded and created.
            // Textures may continue to stream in while in the LOADED state.
            if (loadResources.pendingBufferLoads === 0) {
                ModelUtility.checkSupportedExtensions(this.extensionsRequired, supportsWebP);

                addBuffersToLoadResources(this);
                parseBufferViews(this);

                this._boundingSphere = ModelUtility.computeBoundingSphere(this);
                this._initialRadius = this._boundingSphere.radius;
                createResources(this, frameState);
            }
            if (loadResources.finished()) {
                this._state = ModelState.LOADED;
                justLoaded = true;
            }
        }

        if (defined(loadResources) && (this._state === ModelState.LOADED)) {
            if (!justLoaded) {
                createResources(this, frameState);
            }

            if (loadResources.finished()) {
                this._loadResources = undefined;  // Clear CPU memory since WebGL resources were created.
            }
        }

        var show = this.show;

        if ((show && this._state === ModelState.LOADED) || justLoaded) {
            this._dirty = false;
            var modelMatrix = this.modelMatrix;

            var modeChanged = frameState.mode !== this._mode;
            this._mode = frameState.mode;

            // ClassificationModel's model matrix needs to be updated
            var modelTransformChanged = !Matrix4.equals(this._modelMatrix, modelMatrix) || modeChanged;

            if (modelTransformChanged || justLoaded) {
                Matrix4.clone(modelMatrix, this._modelMatrix);

                var computedModelMatrix = this._computedModelMatrix;
                Matrix4.clone(modelMatrix, computedModelMatrix);
                if (this._upAxis === Axis.Y) {
                    Matrix4.multiplyTransformation(computedModelMatrix, Axis.Y_UP_TO_Z_UP, computedModelMatrix);
                } else if (this._upAxis === Axis.X) {
                    Matrix4.multiplyTransformation(computedModelMatrix, Axis.X_UP_TO_Z_UP, computedModelMatrix);
                }
            }

            // Update modelMatrix throughout the graph as needed
            if (modelTransformChanged || justLoaded) {
                updateNodeModelMatrix(this, modelTransformChanged, justLoaded, frameState.mapProjection);
                this._dirty = true;
            }
        }

        if (justLoaded) {
            // Called after modelMatrix update.
            var model = this;
            frameState.afterRender.push(function() {
                model._ready = true;
                model._readyPromise.resolve(model);
            });
            return;
        }

        if (show && !this._ignoreCommands) {
            this._primitive.debugShowBoundingVolume = this.debugShowBoundingVolume;
            this._primitive.debugWireframe = this.debugWireframe;
            this._primitive.update(frameState);
        }
    };

    ClassificationModel.prototype.isDestroyed = function() {
        return false;
    };

    ClassificationModel.prototype.destroy = function() {
        this._primitive = this._primitive && this._primitive.destroy();
        return destroyObject(this);
    };
export default ClassificationModel;
